package com.linkdood.app.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
import com.linkdood.app.constants.AttachmentConstants;
import com.linkdood.app.constants.ErrorCode;
import com.linkdood.app.domain.AccountUploadFile;
import com.linkdood.app.domain.Attachment;
import com.linkdood.app.domain.UploadFile;
import com.linkdood.app.dto.AttachmentDTO;
import com.linkdood.app.exception.GeneralException;
import com.linkdood.app.exception.InvalidParamException;
import com.linkdood.app.mapper.AttachmentMapper;
import com.linkdood.app.service.AccountUploadFileService;
import com.linkdood.app.service.AttachmentService;
import com.linkdood.app.service.UploadFileService;
import com.linkdood.app.utils.DateTimeUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.system.ApplicationHome;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.client.RestTemplate;

import javax.net.ssl.SSLContext;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 附件
 * @author YunJ
 */
@Service
@Slf4j
public class AttachmentServiceImpl implements AttachmentService {

    @Value("${customize.upload.path}")
    private String filePath;

    @Value("${server.servlet.context-path}")
    private String contextPath;

    private final AttachmentMapper attachmentMapper;

    private final UploadFileService uploadFileService;

    private final AccountUploadFileService accountUploadFileService;

    public AttachmentServiceImpl(AttachmentMapper attachmentMapper, UploadFileService uploadFileService, AccountUploadFileService accountUploadFileService) {
        this.attachmentMapper = attachmentMapper;
        this.uploadFileService = uploadFileService;
        this.accountUploadFileService = accountUploadFileService;
    }

    @Override
    public Map<String, Boolean> save(List<AttachmentDTO> attachments) {
        Map<String, Boolean> result = new HashMap<>(attachments.size());
        for (AttachmentDTO attachment : attachments) {
            Attachment attach = attachment.transformToAttachment();
            Boolean sqlResult = SqlHelper.retBool(attachmentMapper.insert(attach));
            result.put(attachment.getFilename() + "-" + attach.getId(), sqlResult);
        }
        return result;
    }

    @Override
    public List<Attachment> get() {
        return attachmentMapper.selectList(null);
    }

    @Override
    public String getFileName(String id) {
        Attachment attachment = attachmentMapper.selectById(id);
        if (attachment == null) {
            throw new InvalidParamException("id");
        }
        File fileDir = new File(filePath);
        if (!fileDir.isDirectory()) {
            throw new GeneralException(ErrorCode.ERROR_CONFIGURATION_FILE);
        }
        String[] files = fileDir.list();
        if (files == null) {
            throw new GeneralException(ErrorCode.ERROR_CONFIGURATION_FILE);
        }

        for (String filename : files) {
            if (filename.contains(attachment.getFilename())) {
                return filename;
            }
        }
        return "";
    }

    /**
     * 下载 Attachment 中 的文件
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    @Async
    public void downloadFile(List<String> idList) {
        List<Attachment> attachmentList = attachmentMapper.selectBatchIds(idList);
        syncFile(attachmentList);
    }

    /**
     * 下载 Attachment 中的文件
     *
     * @param attachmentList 文件列表
     */
    @Override
    public void syncFile(List<Attachment> attachmentList) {
        try {
            int syncFileSize = 0;
            File fileDir = new File(filePath);
            FileUtils.forceMkdir(fileDir);
            RestTemplate restTemplate = getRestTemplate();
            for (Attachment attachment : attachmentList) {
                String fileUrl = attachment.getFileUrl();
                String targetPath = filePath + DateTimeUtil.localDateTimeToYMD(attachment.getCreateTime()) + File.separator +
                        attachment.getId() + "_" + attachment.getFilename();
                File ret = new File(targetPath);
                FileUtils.forceMkdirParent(ret);
                File file;
                try {
                    file = restTemplate.execute(fileUrl, HttpMethod.GET, null,
                            clientHttpResponse -> {
                                StreamUtils.copy(clientHttpResponse.getBody(), new FileOutputStream(ret));
                                return ret;
                            });
                } catch (Exception e) {
                    log.warn("download attachment file failed, id: {}, name: {}", attachment.getId(), attachment.getFilename());
                    continue;
                }

                if (file != null && file.isFile()) {
                    syncFileSize += 1;
                    attachment.setFilePath(targetPath);
                    attachment.setStatus(AttachmentConstants.ATTACHMENT_STATUS_DOWNLOAD_SUCCESS);
                    attachment.setFileSize(file.length());

                    UploadFile uploadFile = new UploadFile();
                    uploadFile.setName(attachment.getFilename());
                    uploadFile.setPath(attachment.getFilePath());
                    uploadFile.setSize(attachment.getFileSize());
                    uploadFileService.save(uploadFile);

                    if (StringUtils.isNotBlank(attachment.getAccountId())) {
                        AccountUploadFile accountUploadFile = new AccountUploadFile();
                        accountUploadFile.setAccountId(attachment.getAccountId());
                        accountUploadFile.setFileId(uploadFile.getId());
                        accountUploadFileService.save(accountUploadFile);
                    }
                } else {
                    attachment.setStatus(AttachmentConstants.ATTACHMENT_STATUS_DOWNLOAD_FAIL);
                }
                attachmentMapper.updateById(attachment);
            }
            log.debug("download attachment file success, number is {}", syncFileSize);
        } catch (Exception e) {
            log.info("download attachment file occur error: {}", e.getMessage());
        }
    }

    /**
     * 删除文件下载成功的记录
     */
    @Override
    public void deleteSuccessFile() {
        LambdaQueryWrapper<Attachment> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Attachment::getStatus, AttachmentConstants.ATTACHMENT_STATUS_DOWNLOAD_SUCCESS);
        int result = attachmentMapper.delete(queryWrapper);
        log.debug("delete success file records, number is {}", result);
    }

    /**
     * 定时同步下载 attachment 中未下载的文件
     */
    @Override
    public void syncAttachmentFile() {
        LambdaQueryWrapper<Attachment> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Attachment::getStatus, AttachmentConstants.ATTACHMENT_STATUS_INTI)
                .or()
                .eq(Attachment::getStatus, AttachmentConstants.ATTACHMENT_STATUS_DOWNLOAD_FAIL);
        List<Attachment> attachmentList = attachmentMapper.selectList(queryWrapper);
        syncFile(attachmentList);
        deleteSuccessFile();
    }

    @Async
    @Override
    public void decryptByBase64(String base64, String fileName, String accountId) {
        if (StringUtils.isEmpty(base64) && StringUtils.isEmpty(fileName)) {
            throw new InvalidParamException();
        }
        File fileDir = new File(filePath);
        if (!fileDir.isDirectory()) {
            throw new GeneralException(ErrorCode.ERROR_CONFIGURATION_FILE);
        }
        try {
            Attachment attachment = new Attachment();
            String targetPath = filePath + DateTimeUtil.nowFormatToYMD() + File.separator + uploadFileService.getUUIDFileName(fileName);
            File ret = new File(targetPath);
            FileUtils.forceMkdirParent(ret);
            base64 = base64.replaceAll(" ", "");
            base64 = base64.replaceAll("(\r\n|\r|\n|\n\r)", "");

            Files.write(Paths.get(targetPath), Base64.getDecoder().decode(base64), StandardOpenOption.CREATE);

            File file = new File(targetPath);
            if (!file.exists()) {
                throw new GeneralException(ErrorCode.ERROR_LOGS_FILE_NOT_FOUND);
            }
            attachment.setFileUrl("");
            attachment.setFilename(file.getName());
            attachment.setFilePath(targetPath);
            attachment.setStatus(AttachmentConstants.ATTACHMENT_STATUS_DOWNLOAD_SUCCESS);
            attachment.setFileSize(file.length());
            attachment.setAccountId(accountId);
            attachmentMapper.insert(attachment);

            UploadFile uploadFile = new UploadFile();
            uploadFile.setName(attachment.getFilename());
            uploadFile.setSize(attachment.getFileSize());
            uploadFile.setPath(attachment.getFilePath());
            uploadFileService.save(uploadFile);

            if (StringUtils.isNotBlank(accountId)) {
                AccountUploadFile accountUploadFile = new AccountUploadFile();
                accountUploadFile.setFileId(uploadFile.getId());
                accountUploadFile.setAccountId(attachment.getAccountId());
                accountUploadFileService.save(accountUploadFile);
            }
        } catch (IOException e) {
            log.error("decryptByBase64 failed : {}", e.getMessage());
        }
    }

    /**
     * 大文件上传接口
     * @param base64
     * @param burstSize
     * @param fileIndex
     * @param fileNumber
     * @param fileId
     * @param fileName
     * @param fileHash
     * @throws IOException
     */
    @Override
    public Map<String, String> saveBase64(@RequestParam String base64, @RequestParam Long burstSize,
                                                  @RequestParam Long fileIndex, @RequestParam Long fileNumber,
                                                  @RequestParam String fileId, @RequestParam String fileName,
                                                  @RequestParam String fileHash) throws IOException {
        if (StringUtils.isBlank(base64) || burstSize == null || fileIndex == null || fileNumber == null
                || StringUtils.isBlank(fileId) || StringUtils.isBlank(fileName) || StringUtils.isBlank(fileHash)) {
            throw new InvalidParamException();
        }

        // FIXME(MJX) 之后还要考虑当磁盘空间满了之后该如何处理

        ApplicationHome h = new ApplicationHome(getClass());
        File jarFile = h.getSource();
        String jarPath = jarFile.getParentFile().toString();
        String childPath = filePath + "files/";
        File upload = new File(jarPath, childPath);
        if (!upload.exists()) {
            upload.mkdirs();
        }

        // 获取保存文件名
        String suffix = fileName.lastIndexOf(".") > -1 ? fileName
                .substring(fileName.lastIndexOf(".")) : "";
        String saveFileName = fileId + suffix;
        String resourcePath = upload.getAbsolutePath();
        File file = new File(resourcePath + "/", saveFileName);
        RandomAccessFile accessTmpFile = new RandomAccessFile(file, "rw");

        // 计算分片偏移量
        long offset = burstSize * fileIndex;
        // 定位到该分片的偏移量
        accessTmpFile.seek(offset);
        // 写入该分片数据
        try {
            // 安卓大文件上传保存文件
            accessTmpFile.write(Base64.getDecoder().decode(base64.getBytes(StandardCharsets.UTF_8)));
        } catch (Exception e) {
            // 兼容苹果大文件上传保存文件
            accessTmpFile.write(Base64.getMimeDecoder().decode(base64.getBytes(StandardCharsets.UTF_8)));
        } finally {
            // 释放
            accessTmpFile.close();
        }
        // 将文件信息加入到 uploadFile 中做统一管理
        List<UploadFile> uploadFiles = uploadFileService.listFileByPath(file.toString());
        if (CollectionUtils.isEmpty(uploadFiles)) {
            UploadFile uploadFile = new UploadFile();
            uploadFile.setName(file.getName());
            uploadFile.setPath(file.toString());
            uploadFileService.save(uploadFile);
        }

        // 上传结果
        Map<String, String> result = new HashMap<>();
        String filePath = contextPath + "/api/staticFile/files/" + saveFileName;
        result.put("filePath", filePath);

        return result;
    }

    /**
     * 设置忽略 ssl 证书
     * 下载的链接是 https 请求，但是链接域名的证书有问题，会导致请求后出现 SSLPeerUnverifiedException 异常，所以设置忽略 ssl 证书
     */
    @Override
    public RestTemplate getRestTemplate() throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
        TrustStrategy acceptingTrustStrategy = (X509Certificate[] chain, String authType) -> true;
        SSLContext sslContext = org.apache.http.ssl.SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();
        SSLConnectionSocketFactory connectionSocketFactory = new SSLConnectionSocketFactory(sslContext, new NoopHostnameVerifier());
        CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(connectionSocketFactory).build();
        HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
        requestFactory.setHttpClient(httpClient);
        requestFactory.setConnectTimeout(15000);
        requestFactory.setReadTimeout(5000);
        return new RestTemplate(requestFactory);
    }

}
